package org.apache.lucene.analysis;

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.io.IOException;
import java.io.Reader;

import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.util.AttributeSource;
import org.apache.lucene.util.CharacterUtils;
import org.apache.lucene.util.Version;
import org.apache.lucene.util.VirtualMethod;
import org.apache.lucene.util.CharacterUtils.CharacterBuffer;

/**
 * An abstract base class for simple, character-oriented tokenizers. 
 * <p>
 * <a name="version">You must specify the required {@link Version} compatibility
 * when creating {@link CharTokenizer}:
 * <ul>
 * <li>As of 3.1, {@link CharTokenizer} uses an int based API to normalize and
 * detect token codepoints. See {@link #isTokenChar(int)} and
 * {@link #normalize(int)} for details.</li>
 * </ul>
 * <p>
 * A new {@link CharTokenizer} API has been introduced with Lucene 3.1. This API
 * moved from UTF-16 code units to UTF-32 codepoints to eventually add support
 * for <a href=
 * "http://java.sun.com/j2se/1.5.0/docs/api/java/lang/Character.html#supplementary"
 * >supplementary characters</a>. The old <i>char</i> based API has been
 * deprecated and should be replaced with the <i>int</i> based methods
 * {@link #isTokenChar(int)} and {@link #normalize(int)}.
 * </p>
 * <p>
 * As of Lucene 3.1 each {@link CharTokenizer} - constructor expects a
 * {@link Version} argument. Based on the given {@link Version} either the new
 * API or a backwards compatibility layer is used at runtime. For
 * {@link Version} < 3.1 the backwards compatibility layer ensures correct
 * behavior even for indexes build with previous versions of Lucene. If a
 * {@link Version} >= 3.1 is used {@link CharTokenizer} requires the new API to
 * be implemented by the instantiated class. Yet, the old <i>char</i> based API
 * is not required anymore even if backwards compatibility must be preserved.
 * {@link CharTokenizer} subclasses implementing the new API are fully backwards
 * compatible if instantiated with {@link Version} < 3.1.
 * </p>
 * <p>
 * <strong>Note:</strong> If you use a subclass of {@link CharTokenizer} with {@link Version} >=
 * 3.1 on an index build with a version < 3.1, created tokens might not be
 * compatible with the terms in your index.
 * </p>
 **/
public abstract class CharTokenizer extends Tokenizer {

    /**
     * Creates a new {@link CharTokenizer} instance
     * 
     * @param matchVersion
     *          Lucene version to match See {@link <a href="#version">above</a>}
     * @param input
     *          the input to split up into tokens
     */
    public CharTokenizer(Version matchVersion, Reader input) {
        super(input);
        charUtils = CharacterUtils.getInstance(matchVersion);
        useOldAPI = useOldAPI(matchVersion);

    }

    /**
     * Creates a new {@link CharTokenizer} instance
     * 
     * @param matchVersion
     *          Lucene version to match See {@link <a href="#version">above</a>}
     * @param source
     *          the attribute source to use for this {@link Tokenizer}
     * @param input
     *          the input to split up into tokens
     */
    public CharTokenizer(Version matchVersion, AttributeSource source, Reader input) {
        super(source, input);
        charUtils = CharacterUtils.getInstance(matchVersion);
        useOldAPI = useOldAPI(matchVersion);
    }

    /**
     * Creates a new {@link CharTokenizer} instance
     * 
     * @param matchVersion
     *          Lucene version to match See {@link <a href="#version">above</a>}
     * @param factory
     *          the attribute factory to use for this {@link Tokenizer}
     * @param input
     *          the input to split up into tokens
     */
    public CharTokenizer(Version matchVersion, AttributeFactory factory, Reader input) {
        super(factory, input);
        charUtils = CharacterUtils.getInstance(matchVersion);
        useOldAPI = useOldAPI(matchVersion);
    }

    /**
     * Creates a new {@link CharTokenizer} instance
     * @param input the input to split up into tokens
     * @deprecated use {@link #CharTokenizer(Version, Reader)} instead. This will be
     *             removed in Lucene 4.0.
     */
    @Deprecated
    public CharTokenizer(Reader input) {
        this(Version.LUCENE_30, input);
    }

    /**
     * Creates a new {@link CharTokenizer} instance
     * @param input the input to split up into tokens
     * @param source the attribute source to use for this {@link Tokenizer}
     * @deprecated use {@link #CharTokenizer(Version, AttributeSource, Reader)} instead. This will be
     *             removed in Lucene 4.0.
     */
    @Deprecated
    public CharTokenizer(AttributeSource source, Reader input) {
        this(Version.LUCENE_30, source, input);
    }

    /**
     * Creates a new {@link CharTokenizer} instance
     * @param input the input to split up into tokens
     * @param factory the attribute factory to use for this {@link Tokenizer}
     * @deprecated use {@link #CharTokenizer(Version, AttributeSource.AttributeFactory, Reader)} instead. This will be
     *             removed in Lucene 4.0.
     */
    @Deprecated
    public CharTokenizer(AttributeFactory factory, Reader input) {
        this(Version.LUCENE_30, factory, input);
    }

    private int offset = 0, bufferIndex = 0, dataLen = 0, finalOffset = 0;
    private static final int MAX_WORD_LEN = 255;
    private static final int IO_BUFFER_SIZE = 4096;

    private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);;
    private final OffsetAttribute offsetAtt = addAttribute(OffsetAttribute.class);

    private final CharacterUtils charUtils;
    private final CharacterBuffer ioBuffer = CharacterUtils.newCharacterBuffer(IO_BUFFER_SIZE);

    /**
     * @deprecated this will be removed in lucene 4.0
     */
    @Deprecated
    private final boolean useOldAPI;

    /**
     * @deprecated this will be removed in lucene 4.0
     */
    @Deprecated
    private static final VirtualMethod<CharTokenizer> isTokenCharMethod = new VirtualMethod<CharTokenizer>(CharTokenizer.class, "isTokenChar", char.class);

    /**
     * @deprecated this will be removed in lucene 4.0
     */
    @Deprecated
    private static final VirtualMethod<CharTokenizer> normalizeMethod = new VirtualMethod<CharTokenizer>(CharTokenizer.class, "normalize", char.class);

    /**
     * Returns true iff a UTF-16 code unit should be included in a token. This
     * tokenizer generates as tokens adjacent sequences of characters which
     * satisfy this predicate. Characters for which this is <code>false</code> are
     * used to define token boundaries and are not included in tokens.
     * <p>
     * Note: This method cannot handle <a href=
     * "http://java.sun.com/j2se/1.5.0/docs/api/java/lang/Character.html#supplementary"
     * >supplementary characters</a>. To support all Unicode characters, including
     * supplementary characters, use the {@link #isTokenChar(int)} method.
     * </p>
     * 
     * @deprecated use {@link #isTokenChar(int)} instead. This method will be
     *             removed in Lucene 4.0.
     */
    @Deprecated
    protected boolean isTokenChar(char c) {
        return isTokenChar((int) c);
    }

    /**
     * Called on each token UTF-16 code unit to normalize it before it is added to the
     * token. The default implementation does nothing. Subclasses may use this to,
     * e.g., lowercase tokens.
     * <p>
     * Note: This method cannot handle <a href=
     * "http://java.sun.com/j2se/1.5.0/docs/api/java/lang/Character.html#supplementary"
     * >supplementary characters</a>. To support all Unicode characters, including
     * supplementary characters, use the {@link #normalize(int)} method.
     * </p>
     * 
     * @deprecated use {@link #normalize(int)} instead. This method will be
     *             removed in Lucene 4.0.
     */
    @Deprecated
    protected char normalize(char c) {
        return (char) normalize((int) c);
    }

    /**
     * Returns true iff a codepoint should be included in a token. This tokenizer
     * generates as tokens adjacent sequences of codepoints which satisfy this
     * predicate. Codepoints for which this is false are used to define token
     * boundaries and are not included in tokens.
     * <p>
     * As of Lucene 3.1 the char based API ({@link #isTokenChar(char)} and
     * {@link #normalize(char)}) has been depreciated in favor of a Unicode 4.0
     * compatible int based API to support codepoints instead of UTF-16 code
     * units. Subclasses of {@link CharTokenizer} must not override the char based
     * methods if a {@link Version} >= 3.1 is passed to the constructor.
     * <p>
     * <p>
     * NOTE: This method will be marked <i>abstract</i> in Lucene 4.0.
     * </p>
     */
    protected boolean isTokenChar(int c) {
        throw new UnsupportedOperationException("since LUCENE_31 subclasses of CharTokenizer must implement isTokenChar(int)");
    }

    /**
     * Called on each token character to normalize it before it is added to the
     * token. The default implementation does nothing. Subclasses may use this to,
     * e.g., lowercase tokens.
     * <p>
     * As of Lucene 3.1 the char based API ({@link #isTokenChar(char)} and
     * {@link #normalize(char)}) has been depreciated in favor of a Unicode 4.0
     * compatible int based API to support codepoints instead of UTF-16 code
     * units. Subclasses of {@link CharTokenizer} must not override the char based
     * methods if a {@link Version} >= 3.1 is passed to the constructor.
     * <p>
     * <p>
     * NOTE: This method will be marked <i>abstract</i> in Lucene 4.0.
     * </p>
     */
    protected int normalize(int c) {
        return c;
    }

    @Override
    public final boolean incrementToken() throws IOException {
        clearAttributes();
        if (useOldAPI) // TODO remove this in LUCENE 4.0
            return incrementTokenOld();
        int length = 0;
        int start = -1; // this variable is always initialized
        char[] buffer = termAtt.buffer();
        while (true) {
            if (bufferIndex >= dataLen) {
                offset += dataLen;
                if (!charUtils.fill(ioBuffer, input)) { // read supplementary char aware with CharacterUtils
                    dataLen = 0; // so next offset += dataLen won't decrement offset
                    if (length > 0) {
                        break;
                    } else {
                        finalOffset = correctOffset(offset);
                        return false;
                    }
                }
                dataLen = ioBuffer.getLength();
                bufferIndex = 0;
            }
            // use CharacterUtils here to support < 3.1 UTF-16 code unit behavior if the char based methods are gone
            final int c = charUtils.codePointAt(ioBuffer.getBuffer(), bufferIndex);
            bufferIndex += Character.charCount(c);

            if (isTokenChar(c)) { // if it's a token char
                if (length == 0) { // start of token
                    assert start == -1;
                    start = offset + bufferIndex - 1;
                } else if (length >= buffer.length - 1) { // check if a supplementary could run out of bounds
                    buffer = termAtt.resizeBuffer(2 + length); // make sure a supplementary fits in the buffer
                }
                length += Character.toChars(normalize(c), buffer, length); // buffer it, normalized
                if (length >= MAX_WORD_LEN) // buffer overflow! make sure to check for >= surrogate pair could break == test
                    break;
            } else if (length > 0) // at non-Letter w/ chars
                break; // return 'em
        }

        termAtt.setLength(length);
        assert start != -1;
        offsetAtt.setOffset(correctOffset(start), finalOffset = correctOffset(start + length));
        return true;

    }

    /**
     * The <= 3.0 version of incrementToken. This is a backwards compat implementation used
     * if a version <= 3.0 is provided to the ctor. 
     * @deprecated remove in 4.0
     */
    @Deprecated
    private boolean incrementTokenOld() throws IOException {
        int length = 0;
        int start = -1; // this variable is always initialized
        char[] buffer = termAtt.buffer();
        final char[] oldIoBuffer = ioBuffer.getBuffer();
        while (true) {

            if (bufferIndex >= dataLen) {
                offset += dataLen;
                dataLen = input.read(oldIoBuffer);
                if (dataLen == -1) {
                    dataLen = 0; // so next offset += dataLen won't decrement offset
                    if (length > 0) {
                        break;
                    } else {
                        finalOffset = correctOffset(offset);
                        return false;
                    }
                }
                bufferIndex = 0;
            }

            final char c = oldIoBuffer[bufferIndex++];

            if (isTokenChar(c)) { // if it's a token char

                if (length == 0) { // start of token
                    assert start == -1;
                    start = offset + bufferIndex - 1;
                } else if (length == buffer.length) {
                    buffer = termAtt.resizeBuffer(1 + length);
                }

                buffer[length++] = normalize(c); // buffer it, normalized

                if (length == MAX_WORD_LEN) // buffer overflow!
                    break;

            } else if (length > 0) // at non-Letter w/ chars
                break; // return 'em
        }

        termAtt.setLength(length);
        assert start != -1;
        offsetAtt.setOffset(correctOffset(start), correctOffset(start + length));
        return true;
    }

    @Override
    public final void end() {
        // set final offset
        offsetAtt.setOffset(finalOffset, finalOffset);
    }

    @Override
    public void reset(Reader input) throws IOException {
        super.reset(input);
        bufferIndex = 0;
        offset = 0;
        dataLen = 0;
        finalOffset = 0;
        ioBuffer.reset(); // make sure to reset the IO buffer!!
    }

    /**
     * @deprecated this will be removed in lucene 4.0
     */
    @Deprecated
    private boolean useOldAPI(Version matchVersion) {
        final Class<? extends CharTokenizer> clazz = this.getClass();
        if (matchVersion.onOrAfter(Version.LUCENE_31) && (isTokenCharMethod.isOverriddenAsOf(clazz) || normalizeMethod.isOverriddenAsOf(clazz)))
            throw new IllegalArgumentException("For matchVersion >= LUCENE_31, CharTokenizer subclasses must not override isTokenChar(char) or normalize(char).");
        return !matchVersion.onOrAfter(Version.LUCENE_31);
    }
}